/* 

==========================================================

DX490a - Summer 2010

Instructor: Stelios Manousakis

==========================================================

Class 6.2:

Mapping Strategies

Contents:

• Simple mapping

A few different treatments

- Using math

- Using SimpleNumber methods

- Using ControlSpec

- Using Envelopes

• More complex mapping strategies

• Pseudo-cognitive mapping

• Mapping data to parameters

• Mapping in layers

• Direct vs 'modulation' mapping

==========================================================

*/



// ================= MAPPING STRATEGIES =================


// ====== SIMPLE MAPPING ======


// ------ A few different treatments, using simple math --


// A Routine to simulate incoming data: 

(

~dataMap = {

r = Routine {

loop {

100.do{ arg i;

// what comes in, goes out:

i.postln;

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value

r.stop



//•  shift the data

(

~dataMap = {arg shift;

r = Routine {

loop {

100.do{ arg i;

// shifted/transposed data

(i + shift).postln;

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value(1000)

r.stop;



// • invert the data

(

~dataMap = {

r = Routine {

loop {

100.do{ arg i;

// inverted data

((i * -1) + 100).postln;

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value(1000)

r.stop;


// • compress/expand the data

(

~dataMap = {arg mult;

r = Routine {

loop {

100.do{ arg i;

// compressed/expanded data

(i * mult).postln;

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value(0.10)

r.stop;



// • limit the data

(

~dataMap = {arg limits;

r = Routine {

loop {

100.do{ arg i;

// limit/clip the data

i.clip(limits[0], limits[1]).postln;

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value([20, 50])

r.stop;



// • quantize the data

(

~dataMap = {arg roundTo;

r = Routine {

loop {

100.do{ arg i;

// quantize the data

i.round(roundTo).postln;

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value(10)

r.stop;


// • segment the data

(

~dataMap = {

r = Routine {

loop {

100.do{ arg i;

// segment the data: split into different parameters

c = case

{i < 25} {"send to param 1: ".post; i.postln}

{i > 25 and: {i < 50}} {"send to param 2: ".post; i.postln}

{i > 51 and: {i < 101}} {"send to param 3: ".post; i.postln};

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value

r.stop;




// ------ Using SimpleNumber methods --

// There are also a lot of methods you can call on a SimpleNumber that can be very useful for mapping. There are many Unary and several Binary operators (open up the SimpleNumber help, or press Cmd-y on the class  to view them):

// For example, here are some Unary methods:

(

~dataMap = {

r = Routine {

loop {

100.do{ arg i;

// try out a few different methods:

i.exp.postln;

// i.sin.postln;

// i.cos.postln;

//i.reciprocal.postln;

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value

r.stop;


// you could even call 'audio-processing' methods (like distortion) if you like:

(

~dataMap = {

r = Routine {

loop {

100.do{ arg i;

// first scale down range, say from 0-5

i = i * 0.05;

// try out different methods

//i.distort.postln;

i.softclip.postln;

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value

r.stop;



// There are quite a few useful binary operators, as well:

// For example:

/* linlin(inMin, inMax, outMin, outMax) : linear to linear mapping

linexp(inMin, inMax, outMin, outMax): linear to exponential mapping

explin(inMin, inMax, outMin, outMax): exponential to linear mapping

expexp(inMin, inMax, outMin, outMax): exponential to exponential mapping

lincurve(inMin = 0, inMax = 1, outMin = 0, outMax = 1, curve = -4, clip = \minmax)

*/


(

~dataMap = {arg inMin, inMax, outMin, outMax;

r = Routine {

loop {

100.do{ arg i;

// try a few different ones; note that values above or below the specified 'in' range will be clipped with these methods:

i.linlin(inMin, inMax, outMin, outMax).postln;

// i.linexp(inMin, inMax, outMin, outMax).postln;

// i.explin(inMin, inMax, outMin, outMax).postln;

// i.expexp(inMin, inMax, outMin, outMax).postln;

// (i*0.01).lcurve(1, 0, 1).postln;

// i.gaussCurve(1, 0, 10).postln;

0.01.wait;

}

}

};

r.play

};

)

~dataMap.value(0.000001, 100, 300, 1000)

r.stop;





// ------ Using ControlSpec --

// ControlSpec, a type of Spec (i.e. 'input data specification') is a class most commonly used to map values coming from GUI sliders and knobs, so that their range (which is always 0-1) can be converted to another range and with a specific curve, and then used for a specific type of parameter, such as frequency, amplitude, phase, etc. 


// A ControlSpec (for mapping values coming from a GUI) is created like this:

ControlSpec.new(minval, maxval, warp, step, default, units)

// but you can use it for numbers like this:

ControlSpec.new(minval, maxval, warp, step)

// a nice thing about it, is that it contains the min/max values to map to, a curve function (warp: this is the same as we 've seen in Env; you can look at Warp), as well as a  rounding function (step)

// You can also use a shortcut:

[minVal, maxVal, warp, default, units].asSpec; 

// or, for non-GUI use:

[minVal, maxVal, warp].asSpec;


(

~dataMap = {arg minval, maxval, warp, step;

// make the spec:

c = ControlSpec(minval, maxval, warp, step);

// make a GUI slider just to see better what happens:

z = EZSlider(label: " test ");

r = Routine {

loop {

100.do{ arg i;

// first map to 0-1 range:

i = i * 0.01;

// then use the controlspec:

i = c.map(i).postln;

z.value_(i);

0.01.wait;

}

}

};

r.play(AppClock)

};

)

~dataMap.value(0.000001, 1, \exp, 0.0000000005);

r.stop;



// As this class is commonly used for controlling synthesis, Spec (and its subclass, ControlSpec) contains a Dictionary of specifications, so you can just call the name of the mapping you want to use. Some names include \freq, \lofreq, \midfreq, \phase, \rq, \midinote, \db, \amp, \pan, \rate, \delay, etc. You can see a full list in the Spec helpfile.

(

~dataMap = {

// make the spec:

d = \rq.asSpec;

// make a GUI slider to see better:

z = EZSlider(label: " test ");

r = Routine {

loop {

100.do{ arg i;

// first map to 0-1 range:

i = i * 0.01;

// then use the controlspec:

i = d.map(i).postln;

z.value_(i);

0.01.wait;

}

}

};

r.play(AppClock)

};

)

~dataMap.value;

r.stop;




// ------ Using Envelopes --

// This is all very good, but you may want something more customizable. One of my favorite methods is to use envelopes, as this allows for more complex mappings, multiple curves and practically makes any kind of response possible:

(

~dataMap = {

// make a GUI slider to see better:

z = EZSlider(label: " test ");

// make an envelope:

e = Env([0, 0.5, 0.85, 1], [1, 3, 1].normalizeSum, [2, -6, 3]).plot;

r = Routine {

loop {

100.do{ arg i;

// first map to 0-1 range:

i = i * 0.01;

// then use the envelope:

i = e[i].postln;

z.value_(i);

0.01.wait;

}

}

};

r.play(AppClock)

};

)

~dataMap.value;

r.stop;




// ====== MORE COMPLEX MAPPING STRATEGIES ======


// Apart from the above types of mapping, it may be useful to manipute incoming data in more radical manners.


// • Non-linear transformations can be very useful, and are easy to implement using envelopes.

(

~dataMap = {

// make a GUI slider to see better:

z = EZSlider(label: " test ");

// make an envelope:

e = Env([0, 0.5, 0.3, 0.9, 0.05, 1], [1, 3, 2, 4, 1].normalizeSum, [2, -6, 3, 12, -4]).plot;

r = Routine {

loop {

100.do{ arg i;

// first map to 0-1 range:

i = i * 0.01;

// then use the envelope:

i = e[i].postln;

z.value_(i);

0.01.wait;

}

}

};

r.play(AppClock)

};

)

~dataMap.value;

r.stop;



// • You may want to add some delay to the response

(

// a delay function:

~delay = {|in, delay|

var routine;

routine = Routine{

delay.wait;

{z.value_(in)}.defer; // just for viewing

}.play

};


// let's simulate some input

~data = {arg dataSpeed, delay;

// make a GUI slider to see better:

w = Window.new.front;

w.view.decorator = FlowLayout( w.view.bounds, 10@10, 20@5 );

y = EZSlider(w, 350@30, label: " test ");

z = EZSlider(w, 350@30, label: " delayed ");

r = Routine {

loop {

100.do{ arg i;

// create a random value and send it to the interpolator function

x = 0.0.rrand(1);

{y.value_(x)}.defer; // just for viewing

~delay.value(x, delay);

dataSpeed.wait;

}

}

};

r.play

};

)


~data.value(0.5, 0.25);

r.stop;



// • You may want to reduce or expand data (decimation/interpolation). 

// You can do linear interpolation, but I suggest using 'interpolate', a very nice extension from the wslib quark


// Here is a function that will automatically interpolate between a new value and the last interpolated value

(

~interpolator = {|in, steps = 100, interpTime = 1, interpol = \spline|

var memory = [0, 0];

var wait = interpTime / steps;

q.stop; // stop any previous instances

memory.put(0, b);

memory.put(1, in); // this now contains the previous and the current value

// now, interpolate between the previous and the current value

q = Routine{

steps.do{|inc|

b = memory.intAt(inc/steps);

{z.value_(b)}.defer; // just for viewing

(wait).wait;

};

}.play;

};

// let's simulate some input

~data = {arg dataSpeed, interpolSpeed;

// make GUI sliders to see better:

w = Window.new.front;

w.view.decorator = FlowLayout( w.view.bounds, 10@10, 20@5 );

y = EZSlider(w, 350@30, label: " test");

z = EZSlider(w, 350@30, label: " interp");


b = 0.5; // initialization value

r = Routine {

loop {

100.do{ arg i;

// create a random value and send it to the interpolator function

x = 0.0.rrand(1);

{y.value_(x)}.defer; // just for viewing

~interpolator.value(x, 100, interpolSpeed, \spline);

dataSpeed.wait;

}

}

};

r.play

};

)

~data.value(1, 0.25); // this will reach to the input values, as the interpolation time is short

r.stop;

~data.value(1,  2); // this will not reach to the input values, as the interpolation time is longer than the step time

r.stop;


// • Instead of getting the absolute values, it may make more sense to get the rate of change, or acceleration of incoming values:

// Here is a much more expressive than the previously viewed mouse-theremin example, using acceleration to convert movement energy to amplitude

s.boot;

(

play(

{

var f;

f = MouseY.kr(4000, 200, 'exponential', 0.8);

SinOsc.ar(

freq: f+ (f*SinOsc.ar(7,0,0.02)),

mul: Lag.kr(Slope.kr(MouseX.kr(0, 0.1)))

// try uncommenting the original version and commenting the one above

//mul: MouseX.kr(0, 0.9) 

)

}

)

)

// now move your mouse around!



// • Another thing you may want to do - especially with raw sensor data - is to smooth out / filter your input, so that you eliminate noise and weird spikes. A low-pass filter will do the trick. This is what Lag does above; try without it:

(

play(

{

var f;

f = MouseY.kr(4000, 200, 'exponential', 0.8);

SinOsc.ar(

freq: f+ (f*SinOsc.ar(7,0,0.02)),

mul: Slope.kr(MouseX.kr(0, 0.1))

//mul: Lag.kr(Slope.kr(MouseX.kr(0, 0.1)))

// try uncommenting the original version and commenting the one above

//mul: MouseX.kr(0, 0.9) 

)

}

)

)



// ====== PSEUDO-COGNITIVE MAPPING ======




// • Perform statistical analysis on an input to deduce its character. There are two quarks containing some very useful statistic methods that you should download:

// MathLib quark: extStatistics, contains some additional methods for Collection and Sequencable Collection for stats

// SenseWorld quark: contains the SensorData class with several statistics methods, as well as StatUGens, a set of pseudo-UGens to perform statistical operations on the server, which can be very useful with audio signals


(

a = SensorData.new; // some of the methods below are specific to this class

// others need an array: let's use a circular one; with every incoming number we'll rotate it to the left, and the write incoming data to its end;

m = 0!200; // how big should the circular array/window be?

a.ltlen_(200); // use the same window length for both methods

~dataStats = {

var vals, stDev, mean, fluct, peak;

// make a GUI slider to see better:

// make an envelope:

r = Routine {

loop {

// create some random values, with a low-pass distribution

t = Tendency(0, 1.0, \lpRand);

v = t[v];

// now, add this to the SensorData class, and get some stats

a.addValue(v);

"standard deviation is: ".post; 

a.longStdDev.postln;

"mean is: ".post; 

a.longMean.postln;

"fluctuation is: ".post; 

a.fluctuation.postln;

"current peak is: ".post; 

a.lastPeak.postln;

// add it to the circular array, and get some stats

m = m.shift(-1);

m.putLast(v);

"kurtosis is: ".post; 

m.kurtosis.postln;

"skew is: ".post;

m.skew.postln;

0.5.wait;

}

};

r.play

};

)

~dataStats.value;

r.stop;



// • Pattern recognition: You can scan an incoming stream for a specific pattern and do something once you detect that pattern. 

// Type while the Window is in focus and match a word

// The version below does not account for elapsed time, but you could implement that if you wanted to...

~pattern = "match".ascii; // word to match

(

var memory = Array.newClear(~pattern.size); // create an array to store incoming data in. This will work as a circular memory space, where all the incoming data will be written, and which is going to be checked agains the pattern for similarity

w = Window.new("key-tester");

w.view.keyDownAction = { arg view, char, modifiers, unicode, keycode;

memory = memory.shift(-1); //rotate array to discard the oldest item and make space for the newest one

memory.putLast(unicode); //add the newest input as the last item of our circular memory array

if (~pattern == memory, {w.background_(Color.rand)});

};

w.front

)



// ====== MAPPING DATA TO PARAMETERS ======

// No examples for these yet, but you can easily get the idea:


// • one-to-one: one control parameter mapped directly to a synthesis parameter. This is a very typical - and often not powerful enough  - mapping in electronic music. 


// The next two mappings are much more 'physical-like'; instrumental parameters are always more complex with a lot of cross-talk

// • one-to-many: one control parameter mapped to multiple synthesis parameters


// • many-to-one: many control parameters influencing together a synthesis parameter




// ====== MAPPING IN LAYERS ======


// • single-layer: control synthesis directly


// • n-layers: Go from the control parameters to the synthesis parameters through any number of intermediate parameters. For example: control a cognitive parameter, which is mapped to a set of perceptual parameters, which are mapped to synthesis parameters, with networks of mapping existing in-between each layer.



// ====== DIRECT vs 'MODULATION' MAPPING ======

// A control could be mapped directly to a parameter, in which case if there is no incoming data there will be no change, or it could be mapped to control and sculpt an algorithmic process that runs autonomously on its own.